const fs = require('fs')
const assert = require('assert')
const BMP24 = require('gd-bmp').BMP24

const RED = { R: 255, G: 0, B: 0 }
const YELLOW = { R: 255, G: 255, B: 0 }
const GREEN = { R: 0, G: 255, B: 0 }
const BLUE = { R: 135, G: 206, B: 250 }
const BLACK = { R: 255, G: 255, B: 255 }

const AREA_SIZE = 20

class Heatmap {
  constructor(options) {
    assert(options.width)
    assert(options.height)
    assert(options.radius)
    this.options = options
    this.grayMap = new Map
    this.pointMap = new Map
    this.areaDensityMap = new Map
  }

  addPoint(x, y, weight) {
    const key = this._coordinateToKey(x, y)
    this.pointMap.set(key, weight)
    this._increaseAreaDensity(x, y)
  }

  _addPoint(x, y, weight) {
    const w = this.options.width
    const h = this.options.height
    const r = this.options.radius
    const startX = (x >= r) ? x - r : 0
    const endX = (x < w - r) ? x + r : w - 1
    const startY = (y >= r) ? y - r : 0
    const endY = (y < h - r) ? y + r : h - 1
    for (let i = startX; i <= endX; i++) {
      for (let j = startY; j <= endY; j++) {
        let dis = this._distance(x, y, i, j)
        if (dis > r) continue
        let mask = this._calcMask(dis, weight)
        this._updateGrayMap(i, j, mask)
      }
    }
    this._increaseAreaDensity(x, y)
  }

  save(file) {
    const w = this.options.width
    const h = this.options.height
    let img = new BMP24(w, h)
    img.fillRect(0, 0, w - 1, h - 1, 0xffffff)
    this._normalizeWeight()
    this._calcGrayMask()
    let gm = this.grayMap
    const max = this._getMaxGrayScale()
    for (let [coordinate, gray] of gm) {
      let c1
      let c2
      let percent
      let n = gray / max
      if (n >= 1.0) {
        n = 1.0
        c1 = RED
        c2 = RED
        percent = 1.0
      } else if (n >= 0.75) {
        c1 = YELLOW
        c2 = RED
        percent = (n - 0.75) / 0.25
      } else if (n >= 0.5) {
        c1 = GREEN
        c2 = YELLOW
        percent = (n - 0.5) / 0.25
      } else if (n >= 0.25) {
        c1 = BLUE
        c2 = GREEN
        percent = (n - 0.25) / 0.25
      } else {
        c1 = BLACK
        c2 = BLUE
        percent = n / 0.25
      }
      const { x, y } = this._keyToCoordinate(coordinate)
      const B = c2.B * percent + c1.B * (1 - percent)
      const G = c2.G * percent + c1.G * (1 - percent)
      const R = c2.R * percent + c1.R * (1 - percent)
      const color = B + (G << 8) + (R << 16)
      img.drawPoint(x, y, color)
    }
    fs.writeFileSync(file, img.getFileData())
  }

  _calcGrayMask() {
    const w = this.options.width
    const h = this.options.height
    const r = this.options.radius

    for (let [coordinate, weight] of this.pointMap) {
      let { x, y } = this._keyToCoordinate(coordinate)
      const startX = (x >= r) ? x - r : 0
      const endX = (x < w - r) ? x + r : w - 1
      const startY = (y >= r) ? y - r : 0
      const endY = (y < h - r) ? y + r : h - 1
      for (let i = startX; i <= endX; i++) {
        for (let j = startY; j <= endY; j++) {
          let dis = this._distance(x, y, i, j)
          if (dis > r) continue
          let mask = this._calcMask(dis, weight)
          this._updateGrayMap(i, j, mask)
        }
      }
    }
  }

  _normalizeWeight() {
    for (let [key, weight] of this.pointMap) {
      let { x, y } = this._keyToCoordinate(key)
      let area = this._getAreaNumber(x, y)
      let dentity = this.areaDensityMap.get(area)
      this.pointMap.set(key, weight / dentity)
    }
  }

  _increaseAreaDensity(x, y) {
    const areaNumber = this._getAreaNumber(x, y)
    const density = this.areaDensityMap.get(areaNumber) || 0
    this.areaDensityMap.set(areaNumber, density + 1)
  }

  _getAreaNumber(x, y) {
    let i = Math.floor(x / AREA_SIZE)
    let j = Math.floor(y / AREA_SIZE)
    let w = this.options.width / AREA_SIZE
    return j * w + i
  }

  _keyToCoordinate(key) {
    const x = key % this.options.width
    const y = (key - x) / this.options.width
    return { x, y }
  }
  _coordinateToKey(x, y) {
    return y * this.options.width + x
  }

  _updateGrayMap(x, y, mask) {
    let key = this._coordinateToKey(x, y)
    if (this.grayMap.has(key)) {
      let v = this.grayMap.get(key)
      this.grayMap.set(key, v + mask)
    } else {
      this.grayMap.set(key, mask)
    }
  }

  _getMaxGrayScale() {
    let max = 0
    for (let [_, v] of this.grayMap) {
      if (v > max) {
        max = v
      }
    }
    return max
  }

  _distance(x1, y1, x2, y2) {
    const dx = x1 - x2
    const dy = y1 - y2
    return Math.sqrt(dx * dx + dy * dy)
  }

  _calcMask(dis, weight) {
    const r = this.options.radius
    assert(dis >= 0)
    assert(weight >= 0)
    // return 1 * weight * Math.sqrt((r - dis) / r)
    return 1 * weight * ((r - dis) / r)
  }
}

module.exports = Heatmap